// Copyright (c) 2025, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'package:cfg/ir/constant_value.dart';
import 'package:cfg/ir/flow_graph.dart';
import 'package:cfg/ir/functions.dart';
import 'package:cfg/ir/instructions.dart';
import 'package:cfg/ir/local_variable.dart';
import 'package:cfg/ir/source_position.dart';
import 'package:cfg/ir/types.dart';
import 'package:kernel/ast.dart'
    as ast
    show DartType, Name, VariableDeclaration;

/// Helper class to create IR instructions and populate [FlowGraph].
///
/// Maintains current basic block and appends newly created
/// instructions to the end of the current block.
///
/// Simulates expression stack. When creating IR instructions,
/// their inputs are populated from the expression stack.
///
/// Keeps current source position which is used for all
/// created IR instructions.
///
/// Maintains a stack of entered try-blocks (exception handlers).
/// All newly created basic blocks get a top exception handler.
///
/// All other attributes and optional inputs are passed explicitly
/// when creating IR instructions.
class FlowGraphBuilder {
  /// Current graph being constructed.
  final FlowGraph graph;

  /// Source position for the new instructions.
  SourcePosition currentSourcePosition = noPosition;

  /// Simulated expression stack.
  final List<Definition> stack = [];

  /// Last instruction in the current block.
  Instruction? _last;

  /// Stack of the exception handlers.
  List<CatchBlock> _exceptionHandlers = [];

  FlowGraphBuilder(CFunction function) : graph = FlowGraph(function) {
    _last = graph.entryBlock;
  }

  /// Finish building CFG.
  FlowGraph done() {
    assert(!hasOpenBlock);
    graph.discoverBlocks();
    return graph;
  }

  /// Start filling [block] with instructions.
  void startBlock(Block block) {
    _last = block;
    if (_exceptionHandlers.isNotEmpty) {
      block.exceptionHandler = _exceptionHandlers.last;
    }
  }

  /// Append [instr] to the current block.
  void appendInstruction(Instruction instr) {
    _last!.appendInstruction(instr);
    _last = instr;
  }

  /// End the current block.
  void endBlock() {
    _last = null;
  }

  /// Returns `true` if there is an unfinished basic block being built.
  bool get hasOpenBlock => _last != null;

  /// Push result of [def] into the expression stack.
  void push(Definition def) {
    stack.add(def);
  }

  /// Pop and return value from the top of the expression stack.
  Definition pop() => stack.removeLast();

  /// Returns value from the top of the expression stack without removing it.
  Definition get stackTop => stack.last;

  /// Pop [count] values from the expression stack and
  /// use them as `first ... first + count - 1` inputs of [instr]
  /// (in the reverse order).
  void popInputs(Instruction instr, int first, int count) {
    for (int i = count - 1; i >= 0; --i) {
      instr.setInputAt(first + i, pop());
    }
  }

  /// Pop [count] values from the expression stack.
  void drop(int count) {
    stack.removeRange(stack.length - count, stack.length);
  }

  /// Create a new [JoinBlock].
  JoinBlock newJoinBlock() => JoinBlock(graph, currentSourcePosition);

  /// Create a new [TargetBlock].
  TargetBlock newTargetBlock() => TargetBlock(graph, currentSourcePosition);

  /// Create a new [CatchBlock].
  CatchBlock newCatchBlock() => CatchBlock(graph, currentSourcePosition);

  /// Append [Goto] to the graph. Ends current block.
  void addGoto(Block target) {
    final instr = Goto(graph, currentSourcePosition);
    appendInstruction(instr);
    final currentBlock = instr.block!;
    assert(currentBlock.successors.isEmpty);
    currentBlock.successors.add(target);
    endBlock();
  }

  /// Append [Branch] to the graph. Ends current block.
  void addBranch(TargetBlock trueSuccessor, TargetBlock falseSuccessor) {
    final instr = Branch(graph, currentSourcePosition, pop());
    appendInstruction(instr);
    final currentBlock = instr.block!;
    assert(currentBlock.successors.isEmpty);
    currentBlock.successors.add(trueSuccessor);
    currentBlock.successors.add(falseSuccessor);
    endBlock();
  }

  /// Append [TryEntry] to the graph. Ends current block.
  void addTryEntry(TargetBlock tryBody, CatchBlock catchBlock) {
    final instr = TryEntry(graph, currentSourcePosition);
    appendInstruction(instr);
    final currentBlock = instr.block!;
    assert(currentBlock.successors.isEmpty);
    currentBlock.successors.add(tryBody);
    currentBlock.successors.add(catchBlock);
    endBlock();
  }

  /// Enter try-block with given [exceptionHandler].
  void enterTryBlock(CatchBlock exceptionHandler) {
    _exceptionHandlers.add(exceptionHandler);
  }

  /// Leave the last entered try-block.
  void leaveTryBlock() {
    _exceptionHandlers.removeLast();
  }

  /// Append [Return] to the graph. Ends current block.
  void addReturn() {
    final value = pop();
    final instr = Return(graph, currentSourcePosition, value);
    appendInstruction(instr);
    endBlock();
  }

  /// Append [Comparison] to the graph.
  Comparison addComparison(ComparisonOpcode op) {
    final right = pop();
    final left = pop();
    final instr = Comparison(graph, currentSourcePosition, op, left, right);
    push(instr);
    appendInstruction(instr);
    return instr;
  }

  /// Append [Constant] to the entry block of the graph.
  Constant addConstant(ConstantValue value) {
    final instr = graph.getConstant(value);
    if (instr.previous == _last) {
      _last = instr;
    }
    push(instr);
    return instr;
  }

  /// Append `null` [Constant].
  Constant addNullConstant() => addConstant(ConstantValue.fromNull());

  /// Append `int` [Constant] with given [value].
  Constant addIntConstant(int value) =>
      addConstant(ConstantValue.fromInt(value));

  /// Append `bool` [Constant] with given [value].
  Constant addBoolConstant(bool value) =>
      addConstant(ConstantValue.fromBool(value));

  /// Append [DirectCall] to the graph.
  DirectCall addDirectCall(CFunction target, int inputCount, CType type) {
    final instr = DirectCall(
      graph,
      currentSourcePosition,
      target,
      inputCount,
      type,
    );
    popInputs(instr, 0, inputCount);
    push(instr);
    appendInstruction(instr);
    return instr;
  }

  /// Append [InterfaceCall] to the graph.
  InterfaceCall addInterfaceCall(
    CFunction interfaceTarget,
    int inputCount,
    CType type,
  ) {
    final instr = InterfaceCall(
      graph,
      currentSourcePosition,
      interfaceTarget,
      inputCount,
      type,
    );
    popInputs(instr, 0, inputCount);
    push(instr);
    appendInstruction(instr);
    return instr;
  }

  /// Append [DynamicCall] to the graph.
  DynamicCall addDynamicCall(
    ast.Name selector,
    DynamicCallKind kind,
    int inputCount,
  ) {
    final instr = DynamicCall(
      graph,
      currentSourcePosition,
      selector,
      kind,
      inputCount,
    );
    popInputs(instr, 0, inputCount);
    push(instr);
    appendInstruction(instr);
    return instr;
  }

  /// Add [LocalVariable] to the graph.
  LocalVariable declareLocalVariable(
    String name,
    ast.VariableDeclaration? declaration,
    CType type,
  ) {
    final v = LocalVariable(
      name,
      declaration,
      graph.localVariables.length,
      type,
    );
    graph.localVariables.add(v);
    return v;
  }

  /// Append [Parameter] instruction to the graph.
  Parameter addParameter(LocalVariable variable) {
    final instr = Parameter(graph, currentSourcePosition, variable);
    appendInstruction(instr);
    return instr;
  }

  /// Append [LoadLocal] to the graph.
  LoadLocal addLoadLocal(LocalVariable variable) {
    final instr = LoadLocal(graph, currentSourcePosition, variable);
    push(instr);
    appendInstruction(instr);
    return instr;
  }

  /// Append [StoreLocal] to the graph.
  ///
  /// If [leaveValueOnStack] is `true`, then the stored value is left
  /// on top of the expression stack.
  void addStoreLocal(LocalVariable variable, {bool leaveValueOnStack = false}) {
    final value = pop();
    final instr = StoreLocal(graph, currentSourcePosition, variable, value);
    if (leaveValueOnStack) {
      push(value);
    }
    appendInstruction(instr);
  }

  /// Append [Throw] taking an exception as input to the graph.
  /// Ends current block.
  void addThrow() {
    final exception = pop();
    final instr = Throw(graph, currentSourcePosition, exception, null);
    appendInstruction(instr);
    endBlock();
  }

  /// Append [Throw] taking an exception and stack trace as inputs to
  /// the graph. Ends current block.
  void addRethrow() {
    final stackTrace = pop();
    final exception = pop();
    final instr = Throw(graph, currentSourcePosition, exception, stackTrace);
    appendInstruction(instr);
    endBlock();
  }

  /// Append [TypeParameters] to the graph.
  ///
  /// Optional [receiver] input should be passed if there are class type
  /// parameters in scope.
  TypeParameters addTypeParameters({Definition? receiver}) {
    final instr = TypeParameters(graph, currentSourcePosition, receiver);
    appendInstruction(instr);
    return instr;
  }

  /// Append [TypeCast] to the graph.
  ///
  /// Optional [typeParameters] input should be passed if tested type
  /// depends on type parameters (not fully instantiated).
  ///
  /// Optional [isChecked] argument indicates if this type cast involves
  /// check at runtime.
  TypeCast addTypeCast(
    CType testedType, {
    Definition? typeParameters,
    bool isChecked = true,
  }) {
    final object = pop();
    final instr = TypeCast(
      graph,
      currentSourcePosition,
      object,
      testedType,
      typeParameters,
      isChecked: isChecked,
    );
    push(instr);
    appendInstruction(instr);
    return instr;
  }

  /// Append [TypeTest] to the graph.
  ///
  /// Optional [typeParameters] input should be passed if tested type
  /// depends on type parameters (not fully instantiated).
  TypeTest addTypeTest(CType testedType, {Definition? typeParameters}) {
    final object = pop();
    final instr = TypeTest(
      graph,
      currentSourcePosition,
      object,
      testedType,
      typeParameters,
    );
    push(instr);
    appendInstruction(instr);
    return instr;
  }

  /// Append [TypeArguments] to the graph.
  ///
  /// Optional [typeParameters] input should be passed if type arguments
  /// depend on type parameters (not fully instantiated).
  TypeArguments addTypeArguments(
    List<ast.DartType> types, {
    Definition? typeParameters,
  }) {
    final instr = TypeArguments(
      graph,
      currentSourcePosition,
      types,
      typeParameters,
    );
    push(instr);
    appendInstruction(instr);
    return instr;
  }

  /// Append [BinaryIntOp] to the graph.
  BinaryIntOp addBinaryIntOp(BinaryIntOpcode op) {
    final right = pop();
    final left = pop();
    final instr = BinaryIntOp(graph, currentSourcePosition, op, left, right);
    push(instr);
    appendInstruction(instr);
    return instr;
  }

  /// Append [UnaryIntOp] to the graph.
  UnaryIntOp addUnaryIntOp(UnaryIntOpcode op) {
    final operand = pop();
    final instr = UnaryIntOp(graph, currentSourcePosition, op, operand);
    push(instr);
    appendInstruction(instr);
    return instr;
  }

  /// Append [BinaryDoubleOp] to the graph.
  BinaryDoubleOp addBinaryDoubleOp(BinaryDoubleOpcode op) {
    final right = pop();
    final left = pop();
    final instr = BinaryDoubleOp(graph, currentSourcePosition, op, left, right);
    push(instr);
    appendInstruction(instr);
    return instr;
  }

  /// Append [UnaryDoubleOp] to the graph.
  UnaryDoubleOp addUnaryDoubleOp(UnaryDoubleOpcode op) {
    final operand = pop();
    final instr = UnaryDoubleOp(graph, currentSourcePosition, op, operand);
    push(instr);
    appendInstruction(instr);
    return instr;
  }
}
